iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

1+1+1>3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~系列 第 10

Day 10 Iceberg 四層結構:讀一張表要打開幾個檔案

  • 分享至 

  • xImage
  •  

昨天把一份 98 MB 的 Parquet 剖開,檔案最後有一塊 metadata 叫 footer,記了每一段 row group(水平切開的一組列)裡每欄的 min/max。WHERE id > 4000000 靠這份統計,五段裡剪掉三段,只讀 1.41 MB

問題是:生產環境一張表很少只有一個檔,常見是幾千到幾百萬個 Parquet 散在 S3 上
SELECT COUNT(*) FROM sales WHERE date = '2026-08-25'
引擎為什麼知道只要打開其中三個檔、另外 999,997 個連 footer 都不用碰?

昨天介紹的單位是 row group,而 footer 是寫在那個 Parquet 的最後,只認得自己裡面那幾段,沒有隔壁那份檔的 min/max。
當有一百萬個檔的時候,不能把一百萬個 footer 都打開再決定要哪些,所以需要有一份檔案外面的目錄:每個 data file 的路徑、它屬於哪個日期(或別的分區)、每欄的 min/max,先寫在別處,引擎看完目錄,才決定要不要去碰那個 Parquet。

這份目錄舊做法寫在資料夾路徑裡,Iceberg 改寫成一棵 metadata 樹。

今天要解決的問題是:

  • 舊做法把篩選寫進資料夾,卡在哪?
  • Iceberg 是什麼?跟 Parquet、Hive 資料夾差在哪?
  • 「一張表」在物件儲存上怎麼會有版本?metadata.json 為什麼欄位幾乎都是複數?
  • manifest list 跟 manifest 為什麼要拆成兩層?
  • 一句 query 走漏斗,最後到底打開幾個檔?

舊做法:篩選條件寫進資料夾路徑

Hive 風格是把篩選條件寫進資料夾路徑,資料按日期分堆,路徑長這樣:

s3://warehouse/sales/date=2026-08-25/file-001.parquet

WHERE date = '2026-08-25' 時,引擎只要請物件儲存列出這個資料夾底下的檔,別天的連檔名都看不到。

這有兩個問題:

  1. 資料夾裡若有幾十萬個檔,光是列出這個 prefix 有哪些物件就很慢。
  2. 資料夾名稱只編得進事先選好的欄,WHERE id > 4000000 這種條件不在路徑裡,引擎還是得把當天每一個 Parquet 的 footer 打開來看。

Iceberg 是什麼

Apache Iceberg 是一種 table format(資料表格式),不是檔案格式,也不是查詢引擎。

Parquet 管的是一個檔裡面資料怎麼排;Hive 資料夾管的是用路徑當目錄。
Iceberg 管的是這張表:現在有哪些檔、schema 是什麼、哪個版本是最新。底下的資料通常還是一堆 Parquet,散在 S3 這類物件儲存上。

物件儲存沒有跨物件的交易
Iceberg 要把「一張表」做成有版本、commit 是原子的,得先決定可變狀態放哪。答案在 metadata.json。

Table Metadata:「表」到底是什麼東西

https://ithelp.ithome.com.tw/upload/images/20260826/20183544YuXSxqdAJp.png

圖上的檔名是示意:v42 是這張表的 metadata 改寫過 42 次;snap-N.avro 是 snapshot N 的 manifest list;m-1.avro 是一份 manifest。最底下的 .parquet 才是資料,上面四層都是 metadata。

S3 上改兩個 key 沒有一起成功或一起失敗這件事
Iceberg 把可變狀態壓縮成一個指標:catalog 裡「這張表 current 的 metadata.json 在哪」。commit 就是對這個指標做一次 compare-and-swap(比較並交換)。其他檔(manifest、Parquet)寫出去就不改。失敗就整次 commit 不算,不會出現一半新、一半舊。

所以 metadata.json 裡很多欄位是複數,再另用一個 id 指「現在用哪一個」:

  • schemas + current-schema-id,不是單一 schema
  • partition-specs + default-spec-id,不是單一 partition-spec
  • snapshots + current-snapshot-id

要留歷史,是因為舊的資料檔是用舊的 spec 寫出來的。讀的時候得用當時的 partition spec 去解它的 partition 值,用當時的 schema id 做欄位對應。Hive metastore 往往只記一份「現在的 schema」,舊檔對不上新欄位,schema evolution 才會那麼痛。

snapshots 每次 commit 長一筆,各自指向一份 manifest list。current-snapshot-id 選其中一個。讀舊版本就是改指到另一個 snapshot,不必另做一套 time travel 機制。

下一節的問題是:一個 snapshot 怎麼記住「我有哪些檔」?

Manifests:為什麼是兩層而不是一層

最笨的做法是一份大清單,列出全表每一個 Parquet。每次 commit 都要重寫這份清單,成本跟表有多大成正比。

Iceberg 拆成兩層:

  • manifest list:一個 snapshot 一份,列出這個 snapshot 用到哪些 manifest
  • manifest:一群 data file 的目錄(路徑、partition、欄位統計)

commit 只重寫真正變動的那幾份 manifest,沒改到的 manifest 被新的 manifest list 重新引用。寫放大變成跟「這次改了多少」成正比,不是跟「全表有多大」成正比。這就是為什麼不能合成一層。

剪枝資訊也照同一套邏輯分兩層:

  • manifest list 的每一列帶 partition summary:這份 manifest 涵蓋的 partition 值範圍、有沒有 null。日期對不上,整份 manifest 不用打開。
  • manifest 的每一列對應一個 data file:file_pathpartitionrecord_countfile_size_in_bytes,加上欄位統計 lower_boundsupper_boundsnull_value_countsvalue_counts

bounds 有一件容易寫錯的事:它是保守的,字串還可能被截斷,只留前幾個 byte。所以 bounds 只能拿來排除檔案(證明這個檔一定沒有目標值),不能拿來確認檔裡一定有。剪枝必須是「證明一定沒有才跳過」。這個不對稱跟昨天 Parquet footer 的 min/max 一樣:可能有,不是一定有。

Scan Planning:把資料結構變成漏斗

前兩節是資料結構,讀一張表時,引擎把它收成一條漏斗,越前面讀的 byte 越少、剪掉的量越大:

  1. metadata.json 用 current-snapshot-id 選一個 snapshot
  2. 打開那份 manifest list,用 partition summary 整份跳過對不上的 manifest
  3. 打開留下來的 manifest,用每個檔的 partition 值再跳過個別 data file
  4. 還在的檔,用 column bounds 再篩一輪(id > 4000000 這種不在路徑裡的條件,在這裡才派得上用場)
  5. 剩下的 Parquet,才交給檔案格式自己:footer 的 row group min/max、page index

標題那句「讀一張表要打開幾個檔案」,答案就是這條漏斗的輸出。開頭那句 WHERE date = '2026-08-25',多數 manifest 在第 2 步就掉了;再加 AND id > 4000000,第 4 步還能再砍一批檔。Hive 資料夾做得到第 2 步的一部分,做不到第 4 步。

兩個接縫值得單獨記

Residual predicate(殘留謂詞) 若檔案就在 date=2026-08-25 這個 partition,date = '2026-08-25' 已被 partition 完全滿足,不必再對每一列重算一次,可以從殘留謂詞裡拿掉。id > 4000000 還在,得繼續往下推到 row group、再進頁面。分區剪枝跟述詞下推的接縫就在這裡,實作漏掉的話,等於 partition 白切了還是每列比一次日期。

Delete files scan planning 不只吐出「要讀哪些 Parquet」,還會把對應的 delete 綁到每個 data file 上。下游拿到的是「檔案 + 要套用的刪除集合」。檔案不能改的時候「這列被刪了」寫在哪,是 D12 的題。

架構的最後一截,已經決定打開這個 Parquet 了,頁面裡怎麼只解該解的列,是明天 D11 的晚物化。

留一個沒有標準答案的問題:一張表 10 萬個 Parquet,manifest 要切幾份?1 個 manifest 存 10 萬列,檔案數少,但每次改都要重寫那一份;10 萬個 manifest 每檔一列,細,metadata 會爆。
這個取捨誰決定、依什麼調?

那就明天見~


上一篇
Day 09 Parquet 是什麼、長什麼樣
下一篇
Day 11 Parquet 晚物化(Late Materialization):先 WHERE 再解碼
系列文
1+1+1>3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言